/*
 * Copyright (C) 2014 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { IOException } from './Error/error'
import { MediaType } from './MediaType'
import { BufferedSource } from "./okio/BufferedSource"
import { Charset } from './utils/Charset'
import { buffer, util } from '@kit.ArkTS'
import { Source } from './okio/Source'

/**
 * A one-shot stream from the origin server to the client application with the raw bytes of the
 * response body. Each response body is supported by an active connection to the webserver. This
 * imposes both obligations and limits on the client application.
 *
 * ### The response body must be closed.
 *
 * Each response body is backed by a limited resource like a socket (live network responses) or
 * an open file (for cached responses). Failing to close the response body will leak resources and
 * may ultimately cause the application to slow down or crash.
 *
 * Both this class and [Response] implement [Closeable]. Closing a response simply
 * closes its response body. If you invoke [Call.execute] or implement [Callback.onResponse] you
 * must close this body by calling any of the following methods:
 *
 * * `Response.close()`
 * * `Response.body().close()`
 * * `Response.body().source().close()`
 * * `Response.body().charStream().close()`
 * * `Response.body().byteStream().close()`
 * * `Response.body().bytes()`
 * * `Response.body().string()`
 *
 * There is no benefit to invoking multiple `close()` methods for the same response body.
 *
 * For synchronous calls, the easiest way to make sure a response body is closed is with a `try`
 * block. With this structure the compiler inserts an implicit `finally` clause that calls
 * [close()][Response.close] for you.
 *
 * ```
 * Call call = client.newCall(request);
 * try (Response response = call.execute()) {
 * ... // Use the response.
 * }
 * ```
 *
 * You can use a similar block for asynchronous calls:
 *
 * ```
 * Call call = client.newCall(request);
 * call.enqueue(new Callback() {
 *   public void onResponse(Call call, Response response) throws IOException {
 *     try (ResponseBody responseBody = response.body()) {
 *     ... // Use the response.
 *     }
 *   }
 *
 *   public void onFailure(Call call, IOException e) {
 *   ... // Handle the failure.
 *   }
 * });
 * ```
 *
 * These examples will not work if you're consuming the response body on another thread. In such
 * cases the consuming thread must call [close] when it has finished reading the response
 * body.
 *
 * ### The response body can be consumed only once.
 *
 * This class may be used to stream very large responses. For example, it is possible to use this
 * class to read a response that is larger than the entire memory allocated to the current process.
 * It can even stream a response larger than the total storage on the current device, which is a
 * common requirement for video streaming applications.
 *
 * Because this class does not buffer the full response in memory, the application may not
 * re-read the bytes of the response. Use this one shot to read the entire response into memory with
 * [bytes] or [string]. Or stream the response with either [source], [byteStream], or [charStream].
 */
export abstract class ResponseBody {
  abstract contentType(): MediaType | null

  /**
   * Returns the number of bytes in that will returned by [bytes], or [byteStream], or -1 if
   * unknown.
   */
  abstract contentLength(): number

  abstract source(): Source

  /**
   * Returns the response as a byte array.
   *
   * This method loads entire response body into memory. If the response body is very large this
   * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a
   * possibility for your response.
   */
  async bytes(): Promise<Uint8Array> {
    return this.source().readBytes()
  }

  /**
   * Streams the response body in chunks.
   * @param callback - Function called with each chunk of data, completion status, or error
   */
  stream(callback: (chunk: Uint8Array, complete?: boolean, error?: Error) => void): void {
    this.source().Stream(callback)
  }
  /**
   * Returns the response as a string.
   *
   * If the response starts with a
   * [Byte Order Mark (BOM)](https://en.wikipedia.org/wiki/Byte_order_mark), it is consumed and
   * used to determine the charset of the response bytes.
   *
   * Otherwise if the response has a `Content-Type` header that specifies a charset, that is used
   * to determine the charset of the response bytes.
   *
   * Otherwise the response bytes are decoded as UTF-8.
   *
   * This method loads entire response body into memory. If the response body is very large this
   * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a
   * possibility for your response.
   */
  async toString(): Promise<string> {
    return this.source().readString()
  }

  private charset() {
    return this.contentType()?.charset(Charset.UTF_8) ?? Charset.UTF_8
  }

  /**
   * Closes the response body and releases any resources associated with it.
   */
  close(): void {
    this.source().close()
  }

  /**
   * Creates a new response body that transmits this string. If contentType is non-null and
   * lacks a charset, this will use UTF-8.
   * @param str - The string content
   * @param contentType - Optional media type
   * @returns A ResponseBody instance containing the string
   */
  static toResponseBody(str: string, contentType: MediaType | null = null): ResponseBody {
    let charset: Charset = Charset.UTF_8
    let finalContentType: MediaType | null = contentType
    
    if (contentType !== null) {
      const resolvedCharset = contentType.charset()
      if (resolvedCharset === null) {
        charset = Charset.UTF_8
        finalContentType = MediaType.toMediaTypeOrNull(`${contentType}; charset=utf-8`)
      } else {
        charset = resolvedCharset
      }
    }
    
    const encoder = new util.TextEncoder(charset.toString())
    const uint8Array = encoder.encodeInto(str)
    const buffer = BufferedSource.fromBytes(uint8Array)
    return ResponseBody.bufferedSourceAsResponseBody(buffer, finalContentType, buffer.bufferedSize)
  }

  /**
   * Creates a new response body that transmits this byte array.
   * @param bytes - The byte array content
   * @param contentType - Optional media type
   * @returns A ResponseBody instance containing the bytes
   */
  static bytesToResponseBody(bytes: Uint8Array, contentType: MediaType | null = null): ResponseBody {
    const buffer = BufferedSource.fromBytes(bytes)
    return ResponseBody.bufferedSourceAsResponseBody(buffer, contentType, bytes.byteLength)
  }

  /**
   * Creates a new response body that transmits from a buffered source.
   * @param bufferSource - The buffered source to read from
   * @param contentType - Optional media type
   * @param contentLength - The content length, or -1 if unknown
   * @returns A ResponseBody instance wrapping the source
   */
  static bufferedSourceAsResponseBody(
    bufferSource: BufferedSource,
    contentType: MediaType | null = null,
    contentLength: number = -1
  ): ResponseBody {
    class ResponseBodyImpl extends ResponseBody {
      contentType(): MediaType | null {
        return contentType
      }
      
      contentLength(): number {
        return contentLength
      }
      
      source(): Source {
        return bufferSource
      }
    }
    return new ResponseBodyImpl()
  }
}
